#!/usr/bin/env python
#
# Copyright (C) 2012 Yazan Boshmaf, Hootan Rashtian, Hyoungshick Kim
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.


"""
An application that uses Facebook Graph API to fetch a user's post and
then performs sentiment analysis on them (positivity in particular).
The sentiments are then correlated with the number of likes marked by
the user's friends. The relationship is visualized in a scatter plot.

Why using this app?
1) See whether your posts are positive or negative
2) See how your friends repond to your posts' positivity

This application is a PoC towards providing tools to understand user engagement
and how to make the platform (and your profile) more attrative to your friends.
"""


"""
Facebook application-specific parameters. You can get these numbers from
the application's webpage using your Facebook developer accounts. Please
replace the values below with yours.
"""
FACEBOOK_APP_ID = "add_app_id_here"
FACEBOOK_APP_SECRET = "add_app_secret_here"
FACEBOOK_APP_EX_PERMISSIONS = "email,read_friendlists,read_stream,publish_stream"


# lower limit on number of posts to fetch
POSTS_LIMIT = 500

# Path to the AFINN word sentiment file
AFINN_PATH = "AFINN/AFINN-111.txt"

# Splash screeen image
LOGO_PATH = "/images/moody_splash.jpg"

# facebook specific
import facebook


# handy packages
import base64
import cgi
import Cookie
import email.utils
import hashlib
import hmac
import logging
import os.path
import time
import urllib
import wsgiref.handlers
import math
import re
import ast
from operator import itemgetter

# Google appengine specific
from google.appengine.ext import db
from google.appengine.ext import webapp
from google.appengine.ext.webapp import util
from google.appengine.ext.webapp import template
from google.appengine.runtime import DeadlineExceededError


# our fav json parser
try:
    import json
    _parse_json = lambda s: json.loads(s)
except ImportError:
    try:
        import simplejson
        _parse_json = lambda s: simplejson.loads(s)
    except ImportError:
        # For Google AppEngine
        from django.utils import simplejson
        _parse_json = lambda s: simplejson.loads(s)


class Moody:
    """Checks how a user's wall posts correlate with the number of likes."""
    def __init__(self, user_record):
        # user related data member
        self.user = user_record        
        self.graph = facebook.GraphAPI(self.user.access_token)
        self.posts = []

        # AFINN related data members
        self.path = AFINN_PATH
        # parse the sentiment file
        self.afinn = dict(map(lambda (w, s): (w, int(s)), [ws.strip().split('\t') for ws in open(self.path) ]))
        # word splitter pattern
        self.pattern_split = re.compile(r"\W+")

        # figure related data members
        self.figure_data = []
        self.corr = 0
        
            
    def parse_wall_data(self, api_call=True, next_url=None):
        """
        Fetches posts from the user's wall. It includes posts that are:
        1) ASCII-only (i.e., use English characters_
        2) Not associated with video or pictures as there is no easy
           way to link the likes with the text but not the media.
        
        Parameters:
        ----------
        
        api_call: set to True if this is the first call of the
                  function. Otherwise, don't specify.
        next_url: url pointing to the next chunk of the user's feed
                  data to be parsed.                
        """

        # stop fetches posts whenever you have at least POSTS_LIMIT
        if len(self.posts) >= POSTS_LIMIT:
            return

        wall_data = None
        
        # use the Graph API to fetch wall data if this is the first call
        if api_call:
            wall_data = self.graph.get_connections(self.user.id, "feed")
        else:
            try: # facebook sometimes blocks request
                data_file = urllib.urlopen(next_url)
            except:
                return
            else: # facebook sometimes returns junk
                try:
                    wall_data = _parse_json(data_file.read())
                finally:
                    data_file.close()

        # if there is not data in the wall, finish
        if wall_data is None:
            return

        # the data key has a value containing all wall info
        if "data" in wall_data:
            data = wall_data["data"]
        else:
            return

        # the paging key has a value containing the anchoring info (urls)
        if "paging" in wall_data:
            paging = wall_data["paging"]
        else:
            return

        # now we have the data and pages. let's start parsing
        for message in data:
            # cosider only those posts that have no links
            if "link" not in message:
                if "message" in message:
                    message_text = message["message"]
                    # consider ascii-only messages
                    if self.is_ascii(message_text):
                        # record number of likes by friends
                        if "likes" in message:
                            message_likes = message["likes"]["count"]
                        else:
                            message_likes = 0
                        # add entry of this post, along with likes count
                        self.posts.append({"message":message_text,"likes":message_likes})

        # if there is a next chunk of posts, then parse them
        if "next" in paging:
            # get the url for the next chunk
            next_url = paging["next"]
            self.parse_wall_data(False, next_url)

                    
    def is_ascii(self, s):
            """Returns True if the string s can be presented in ASCII."""
            return all(ord(c) < 128 for c in s)


    def average(self, x):
            """Returns the average of a list of numbers."""
            assert len(x) > 0
            return float(sum(x)) / len(x)


    def pearson_def(self, x, y):
        """Returns peason correlation coeffecient."""
        assert len(x) == len(y)
        n = len(x)
        assert n > 0
        avg_x = self.average(x)
        avg_y = self.average(y)
        diffprod = 0
        xdiff2 = 0
        ydiff2 = 0
        for idx in range(n):
            xdiff = x[idx] - avg_x
            ydiff = y[idx] - avg_y
            diffprod += xdiff * ydiff
            xdiff2 += xdiff * xdiff
            ydiff2 += ydiff * ydiff
        return diffprod / math.sqrt(xdiff2 * ydiff2)

    def analyze_sentiment(self, text):
        """
        Returns a float for sentiment strength based on the input text.
        Positive values are positive valence, negative value are negative valence. 
        """
        words = self.pattern_split.split(text.lower())
        sentiments = map(lambda word: self.afinn.get(word, 0), words)
        if sentiments:
            # How should you weight the individual word sentiments? 
            # You could do N, sqrt(N) or 1 for example.
            sentiment = float(sum(sentiments))/len(sentiments)
        else:
            sentiment = 0
        return sentiment


    def moodify(self):
        """Computes the mood data and generates the reponse code."""
    	list_score = []
    	list_likes = []
        if len(self.posts) > 0:
            for message in self.posts:
                text = message["message"]
                likes = message["likes"]
                score = self.analyze_sentiment(text)
                self.figure_data.append((score,likes))
                list_score.append(score)
                list_likes.append(likes)
            self.corr = self.pearson_def(list_score, list_likes)
    	return plot_data(figure_data=self.figure_data, corr=self.corr)

def plot_data(figure_data, corr=0, friend_figure_data=None, user_name=None, friend_name=None):
    """Generates a JavaScript to plot data (scatter plot)."""
    num = len(figure_data)

    # if there are no posts, return a message
    if num == 0:
        htmlcode = """
        <html>
        <body>
            <p>Sorry, Could not fetch your wall post! More info? Check out the project's codepage <a href="http://code.google.com/p/fb-moody/" target="_blank">here</a>.</p>
        </body>
        </html>"""
    else: # start with the plotting code
        htmlcode="""
                <html>
                <head>
                <!--Load the AJAX API-->
                <script type="text/javascript" src="https://www.google.com/jsapi"></script>
                <script type="text/javascript">
                google.load('visualization', '1.0', {'packages':['corechart']});
                google.setOnLoadCallback(drawChart);
                function drawChart() {
                  var data = new google.visualization.DataTable();
                  data.addColumn('number', 'Sentiment');"""
        
        # add appropriate column names for the legends
        if friend_figure_data is not None:
             htmlcode += """
                 data.addColumn('number', '"""+user_name+"""');"""
        else:
            htmlcode += """
                 data.addColumn('number', 'Likes');"""

        # if we are comparing with a friend, define a new column
        if friend_figure_data is not None:
            htmlcode += """
                  data.addColumn('number', '"""+friend_name+"""');
                  """
    
        # add the user's figure data      
        data = "data.addRows(["
        for datapoint in figure_data:
            # in case of comparison, we need to add a third null value
            if friend_figure_data is not None:
                data+="["+str(datapoint[0])+", "+str(datapoint[1])+", null],"
            else:
                data+="["+str(datapoint[0])+", "+str(datapoint[1])+"],"
        
        htmlcode += data[:-1]
        htmlcode +="""]);"""

        # if we are comparing the user's data with a friend, add a new set
        if friend_figure_data is not None:
            htmlcode += """
                data.addRows(["""
            data = ""
            for datapoint in friend_figure_data:
                    data+="["+str(datapoint[0])+", null,"+str(datapoint[1])+"],"
            htmlcode += data[:-1]
            htmlcode +="""]);"""

        # continue with plotting
        htmlcode += """
                // Set chart options
                var options = {
                        title: 'Does positivity correlate with likes?',
                        hAxis: {title: 'Positivity'},
                        vAxis: {title: 'Number of likes'},"""

        # remove the legend is not comparing
        if friend_figure_data is None:
            htmlcode += """
                        legend: 'none'
                };"""
        else:
            htmlcode += """
                };"""

        # add the rest of the plotting logic
        htmlcode += """
                // Instantiate and draw our chart, passing in some options.
                var chart = new google.visualization.ScatterChart(document.getElementById('chart_div'));
                chart.draw(data, options);
                }
                </script>
                </head>
                <body>
                <!--Div that will hold the bar chart-->
                <div id="chart_div" style="width:900px; height:500x"></div>
                <br>
                """
        if friend_figure_data is None:
            htmlcode += """
                <p>Number of posts="""+str(num)+""", correlation coeffecient="""+str(corr)+"""</p>
                <p>Wanna compare your mood to your friends? Start by clicking <a href="http://fb-moody.appspot.com/compare/">here</a>.</p>"""
        htmlcode += """
                <p><a href="http://fb-moody.appspot.com/compare/"><<</a> Go back</p>
                </body>
                </html>"""
    return htmlcode


class User(db.Model):
    """ Creates the User db ORM."""
    id = db.StringProperty(required=True)
    created = db.DateTimeProperty(auto_now_add=True)
    updated = db.DateTimeProperty(auto_now=True)
    name = db.StringProperty(required=True)
    profile_url = db.StringProperty(required=True)
    access_token = db.StringProperty(required=True)
    moody = db.BooleanProperty(required=True)
    mood = db.TextProperty(required=False)
	

class BaseHandler(webapp.RequestHandler):
    @property
    def current_user(self):
        """Returns the logged in Facebook user, or None if unconnected."""
        if not hasattr(self, "_current_user"):
            self._current_user = None
            user_id = parse_cookie(self.request.cookies.get("fb_user"))
            if user_id:
                self._current_user = User.get_by_key_name(user_id)
        return self._current_user


class HomeHandler(BaseHandler):
    def get(self):
        """Sends back the home page to the browser."""
        path = os.path.join(os.path.dirname(__file__), "fb-moody.html")
        args = dict(current_user=self.current_user)
        self.response.out.write(template.render(path, args))


class LoginHandler(BaseHandler):
    def get(self):
        """Logs the user in using OAuth 2.0, then downloads the user's info."""
        verification_code = self.request.get("code")
        args = dict(client_id=FACEBOOK_APP_ID, redirect_uri=self.request.path_url, scope=FACEBOOK_APP_EX_PERMISSIONS)
        if verification_code:
            args["client_secret"] = FACEBOOK_APP_SECRET
            args["code"] = verification_code
            response = cgi.parse_qs(urllib.urlopen(
                "https://graph.facebook.com/oauth/access_token?" +
                urllib.urlencode(args)).read())
            access_token = response["access_token"][-1]

            # Download the user profile and cache a local instance of the
            # basic profile info
            profile = json.load(urllib.urlopen(
                "https://graph.facebook.com/me?" +
                urllib.urlencode(dict(access_token=access_token))))
            user = User(key_name=str(profile["id"]), id=str(profile["id"]),
                        name=profile["name"], access_token=access_token,
                        profile_url=profile["link"], moody=False)
            user.put()
            set_cookie(self.response, "fb_user", str(profile["id"]),
                       expires=time.time() + 30 * 86400)
            self.redirect("/")
        else:
            self.redirect(
                "https://graph.facebook.com/oauth/authorize?" +
                urllib.urlencode(args))


class LogoutHandler(BaseHandler):
    def get(self):
        """Logs out the user (kills the cookie). User's data is still stored."""
        set_cookie(self.response, "fb_user", "", expires=time.time() - 86400)
        self.redirect("/")
		
		
class CanvasHandler(BaseHandler):
    def post(self):
        """Splash screen for users checking out the app from Facebook."""
        self.response.out.write("""
          <html>
            <body>
                <p><img src=\""""+LOGO_PATH+"""\"></p>
                <p>Welcome to <b>Facebook Moody!</b> Click <a href="http://fb-moody.appspot.com/">here</a> to start. More info? Check out the project's codepage <a href="http://code.google.com/p/fb-moody/" target="_blank">here</a>.</p>
            </body>
          </html>""")
        
    def get(self):
        """Splash screen for users. Facebook send HTTP POST request."""
        self.response.out.write("""
          <html>
            <body>
                <p><img src=\""""+LOGO_PATH+"""\"></p>
                <p>Welcome to <b>Facebook Moody!</b> Click <a href="http://fb-moody.appspot.com/">here</a> to start. More info? Check out the project's codepage <a href="http://code.google.com/p/fb-moody/" target="_blank">here</a>.</p>
            </body>
          </html>""")


class MoodHandler(BaseHandler):
    def get(self):
        """
        Computes the sentiments of the user posts and returns a javascript
        that visualizes the relationship between number of likes and the
        positivity of the posts.
        """

        try:
            # get the current user.
            user = self.current_user
            
            # get the user's posts.
            moody = Moody(user)
            moody.parse_wall_data()

            # compute the mood data ans response code
            response_data = moody.moodify()

            # add mood data to the user's datastore record
            user.moody = True
            user.mood = str(moody.figure_data)
            user.put()

            # send back the response
            self.response.out.write(response_data)
        except DeadlineExceededError:
            self.response.clear()
            self.response.set_status(500)
            self.response.out.write("""
                                    <html>
                                    <body>
                                    <p>It appears that we have exceeded out quota on Google AppEngine.</p>
                                    <p>This service is provided for free but it limited by available resources.</p>
                                    <p>More info can be found at Google AppEngine <a href="http://code.google.com/appengine/docs/quotas.html" target="_blank">here</a></p>
                                    </body>
                                    </html>""")

def to_list_of_tuples(s):
    """Convers the string into the corresponding list of tuples"""
    stripped_s = str(s).strip("[]")
    return list(ast.literal_eval(stripped_s))
        
class CompareHandler(BaseHandler):
    def get(self):

        # get the user's friends using Graph API
    	user = self.current_user

        # check whether we are comparing figures
    	friend_id = self.request.get("id")
    	if friend_id != "":
            friend_record = User.get_by_key_name(friend_id)
            htmlresponse = plot_data(to_list_of_tuples(user.mood),
                                     friend_figure_data=to_list_of_tuples(friend_record.mood),
                                     user_name=user.name, friend_name=friend_record.name)
            self.response.out.write(htmlresponse)
            
        # otherwise, get the list of friends
        else:
            graph = facebook.GraphAPI(user.access_token)
            friends_response = graph.get_connections(user.id, "friends")

            # make sure that Facebook sent back the friends list
            if "data" in friends_response:
                friends = friends_response["data"]
            else:
                self.response.out.write("""
                            <html>
                            <body>
                            <p>Facebook is acting up! Please try again <a href="http://fb-moody.appspot.com/compare/">here</a>.</p>
                            <p><a href="http://fb-moody.appspot.com/"><<</a> Go back</p>
                            </body>
                            </html>""")
            
            # make sure that the user shared her friends list
            if friends is None:
                self.response.out.write("""
                        <html>
                        <body>
                        <p><img src=\""""+LOGO_PATH+"""\"></p>
                        <p>It appears that you did not share your friends list with us. Sorry, we cannot serve you!</p>
                        <p><a href="http://fb-moody.appspot.com/"><<</a> Go back</p>
                        </body>
                        </html>""")
                    
            friends_ids = []
            for friend in friends:
                friends_ids.append(friend["id"])

            #friend_record = User.get_by_key_name(u"1205718097")

            # get a the list of the user's friends in the datastore with moods 
            friends_records = []
            for friend_id in friends_ids:
                friend_record = User.get_by_key_name(friend_id)
                if friend_record is not None:
                    if friend_record.mood:
                        friends_records.append(friend_record)

            # check if there is a match
            if len(friends_records) == 0:
                self.response.out.write("""
                        <html>
                        <body>
                        <p><img src=\""""+LOGO_PATH+"""\"></p>
                        <p>It appears that no one of your friends tested their mood! Promote this app by clicking <a href="http://fb-moody.appspot.com/promote/">here</a>.</p>
                        <p><a href="http://fb-moody.appspot.com/"><<</a> Go back</p>
                        </body>
                        </html>""")   
            else: # let the user pick one
                htmlresponse = """
                        <html>
                        <body>
                        <p>Pick one of your friends below:</p>
                        <p><table border="0"><tr>"""
                friends_htmlstring = ""
                for friend_record in friends_records:
                    friends_htmlstring = friends_htmlstring + """<td><a href="http://fb-moody.appspot.com/compare/?id="""+friend_record.id+"""\"><img src="http://graph.facebook.com/"""+friend_record.id+"""/picture" /></a></td>"""
                    friends_htmlstring = friends_htmlstring + "<td>&nbsp;</td>"
                    
                friends_htmlstring = friends_htmlstring + "</tr><tr>"
                for friend_record in friends_records:
                    friends_htmlstring = friends_htmlstring + """<td>""" + friend_record.name.split()[0]+ """</td>"""
                    friends_htmlstring = friends_htmlstring + "<td>&nbsp;</td>"

                htmlresponse = htmlresponse + friends_htmlstring + """</tr></table></p>
                                                                      <p><a href="http://fb-moody.appspot.com/"><<</a> Go back</p>
                                                                      </body>
                                                                      </html>"""
                self.response.out.write(htmlresponse)
            
        		
class PromoteHandler(BaseHandler):
    def get(self):
    	user = self.current_user
    	graph = facebook.GraphAPI(user.access_token)
    	attachement = {"name":"Moody for Facebook",
    					"link":"https://apps.facebook.com/fb-moody/",
    					"caption":"Test your positivity among your friends.",
    					"description": "An application that performs sentiment analysis on your wall posts and correltates that with your friends likes.",
    					"picture": "http://icons.iconarchive.com/icons/visualpharm/must-have/256/Positive-icon.png"}
    	msg = "Wonder if your friends like your positive (or negative) posts? Check this out!"
    	graph.put_wall_post(msg, attachement,user.id)
        self.redirect("/")

	
def set_cookie(response, name, value, domain=None, path="/", expires=None):
	"""Generates and signs a cookie for the give name/value"""
	timestamp = str(int(time.time()))
	value = base64.b64encode(value)
	signature = cookie_signature(value, timestamp)
	cookie = Cookie.BaseCookie()
	cookie[name] = "|".join([value, timestamp, signature])
	cookie[name]["path"] = path
	if domain:
		cookie[name]["domain"] = domain
	if expires:
		cookie[name]["expires"] = email.utils.formatdate(expires, localtime=False, usegmt=True)
	response.headers.add("Set-Cookie", cookie.output()[12:])


def parse_cookie(value):
    """Parses and verifies a cookie value from set_cookie"""
    if not value: return None
    parts = value.split("|")
    if len(parts) != 3: return None
    if cookie_signature(parts[0], parts[1]) != parts[2]:
        logging.warning("Invalid cookie signature %r", value)
        return None
    timestamp = int(parts[1])
    if timestamp < time.time() - 30 * 86400:
        logging.warning("Expired cookie %r", value)
        return None
    try:
        return base64.b64decode(parts[0]).strip()
    except:
        return None


def cookie_signature(*parts):
    """Generates a cookie signature.

    We use the Facebook app secret since it is different for every app (so
    people using this example don't accidentally all use the same secret).
    """
    hash = hmac.new(FACEBOOK_APP_SECRET, digestmod=hashlib.sha1)
    for part in parts: hash.update(part)
    return hash.hexdigest()
	
	
def main():
    util.run_wsgi_app(webapp.WSGIApplication([(r"/", HomeHandler),\
                                              (r"/canvas/", CanvasHandler),\
                                              (r"/mood/", MoodHandler),\
                                              (r"/compare/", CompareHandler),\
                                              (r"/promote/", PromoteHandler),\
                                              (r"/auth/login/", LoginHandler),\
                                              (r"/auth/logout/", LogoutHandler)]))


if __name__ == "__main__":
    main()
